Перейти к основному содержимому

5.01. Функции в JavaScript

Разработчику Архитектору

Функции в JavaScript

Что такое функция в JavaScript?

Функция – это блок кода, который выполняет конкретную задачу и может быть повторно использован. Это как мини-программа внутри основной программы.

Как выглядит функция:

function имяФункции(аргументы) {
// Тело функции (код, который выполняется при вызове)
return результат; // Необязательно
}

Что здесь указано?

  • function – это ключевое слово, зарезервированное в языке JS для указания, что сейчас будет функция;
  • имяФункции – это любой набор слов и символов (начинать нужно строчной буквой, ни в коем случае не Прописной и не 1цифрой), который придумывает программист. Поменять можно в любое время, но если где-то идёт обращение к функции, важно поменять и обращение;
  • аргументы – это входные данные, которые принимает функция, словно пакет документов, требуемый в регистратуре – чтобы работать, функция должна знать исходные данные. Их можно и не указывать, тогда скобки с аргументами будут пустыми ();
  • тело функции – это код, который включает в себя почти любой набор кода, который представляет собой суть функции, её всю логику;
  • return – ключевое слово для указания, что сейчас будет возвращаемый результат.

Пример создания:

// Функция для сложения двух чисел
function sum(a, b) {
const result = a + b;
return result; // Возвращаем результат
}

Как можно заметить при разборе:

  • функция называется sum;
  • функция принимает аргументы a и b, следовательно, вызывая её, нужно будет указать значения, где первое значение до запятой будет равным a, второе – b;
  • код включает в себя константу (ключевое слово const) с именем result;
  • константа result равна a + b;
  • результат, который вернёт функция – это то, чему равна константа result.

image-4.png

Так, мы получаем некую функцию, которая суммирует аргументы. И если нам, в какой-то момент, понадобится сложить какие-то два значения, теперь мы можем просто вызывать нашу функцию, сообщим ей значения в виде аргументов.

Пример вызова:

const total = sum(2, 3); // Вызов функции с аргументами 2 и 3
console.log(total); // 5

Здесь мы выполняем какую-то логику, и объявляем константу с именем total, которая равна результату выполнения функции sum(a, b). Указав имя функции и значения аргументов, мы можем даже ссылаться на нашу константу, зная, что она всегда будет равна 5 в нашем случае. И выводим на консоль значение этой константы, выполнив console.log(total).

Ещё раз – повторим, что такое вызов. Понятно, что функция что-то выполняет в своём теле. Но она не запустится сама по себе, а значит кто-то должен её запустить. Этот «запуск» называется вызовом – функцию нужно вызвать. Вызов функции – это момент, когда функция начинает выполнять свой код. Вызов происходит по имени функции с круглыми скобками (и аргументами, если они есть):

// Создали функцию
function greet() {
console.log("Привет!");
}

// Вызвали её 3 раза
greet(); // "Привет!"
greet(); // "Привет!"
greet(); // "Привет!"

Вызывать функции можно сколько угодно раз и когда угодно. Именно тут и раскрывается смысл декомпозиции – нужно разбивать всё на подзадачи и для каждой создавать функции, которые могут пригодиться и упростить код.

Так, мы имеем два понятия - объявление (создание) функции и её вызов.


Объявление функции

В JavaScript существует три способа объявления функции:

  1. Function Declaration Statement - используется ключевое слово function, затем указывается имя функции, параметры, тело и возвращаемое значение:
function <имя> (<Параметры>) {
<тело>
return <возвращаемое значение>
}
  1. Function Definition Expression - функция-выражение, или анонимная функция. Здесь подразумевается, что имени у функции нет:
function (<параметры>) {
<тело>
return <возвращаемое значение>
}

Это удобно, когда нужно создать «вспомогательную функцию», допустим, для передачи в качестве аргумента или записи в переменную. Можно сделать как переменную и получить похожий на вызов синтаксис:

let sum = function(a, b) { return a + b }
let x = sum(2, 2)
  1. Стрелочные функции (или функции-стрелки) - анонимно, без фигурных скобок, с использованием «=>» стрелки:
let <имяПеременной> = (<параметры>) => <тело функции>;

Они позволяют сокращать синтаксис функций, заменяя «{ return result}» на «=> result». Это может быть сложно новичку, но их смысл проще, чем кажется:

Представим, что у нас есть a и b:

let a = 1;
let b = 2;

И нам нужно получить сумму, записав в результат:

let result = sum(a, b);

function sum(a, b) {
return a + b;
}

Мы объявили переменную result, которая равна результату выполнения функции sum, принимающей аргументы a и b, суммирующей их и возвращающей результат. Но можно её записать короче:

let result = (a, b) => a + b;

image-5.png

То есть мы указали аргументы, и буквально упростили функцию, указав её в одной строке. Но для новичка может быть не столь легко читать такие функции. А теперь, если не поняли, перечитайте и подумайте. Принцип таков:

переменная = (аргумент1, аргумент2) => выражение;

С функциями можно делать многие другие вещи, но обо всём по порядку.


Виды функций

Функции бывают нескольких видов:

  1. Чистая функция (Pure Function) – всегда возвращает одинаковый результат для одних и тех же аргументов, и не изменяет внешние переменные.
// Чистая функция
function multiply(a, b) {
return a * b; // Только возвращает результат
}

console.log(multiply(2, 5)); // Всегда 10

Чистой она называется, потому что она независима и даёт результат, строго выполняя свою задачу. Всегда проще представить себе обычные арифметические операции – отличный пример. Выше мы создали чистую функцию multiply(a, b), и в отличие от sum, она умножает a на b. Потом, можем вывести в консоль значение, равное результату функции multiply с аргументами 2 и 5.

  1. Метод – функция, которая является свойством объекта и работает с его данными.
const user = {
name: "Дарт Вейдер",
// Метод объекта
sayHi() {
console.log(`Привет, я ${this.name}!`);
}
};

user.sayHi(); // "Привет, я Дарт Вейдер!"

В примере выше у нас есть объект – user, константа, у которой есть свойство name, равное какому-то значению, и самое главное – внутри объекта user есть функция sayHi().

Она находится внутри объекта, следовательно, просто так к ней не обратиться – она зависима и принадлежит объекту user. Поэтому, вызывая эту функцию sayHi, мы должны указать сначала путь к этой функции – название объекта-владельца. Владелец и свойство/функция разделяются точкой – в нашем случае, это user.sayHi().

Скобки всегда указываются в функциях и методах, даже если аргументов нет. В роли аргументов могут выступать явно указанные данные, переменные/константы:

function greet(name) {
console.log(`Привет, ${name}!`);
}

greet("Йода"); // "Привет, Йода!"
greet("Падме"); // "Привет, Падме!"

После того, как функция выполнит свою работу, она должна что-то вернуть, какой-то результат. «return» возвращает результат (если его нет, функция вернёт неопределённое значение – undefined):

function checkAge(age) {
if (age >= 18) {
return "Доступ разрешён";
} else {
return "Доступ запрещён";
}
}

console.log(checkAge(20)); // "Доступ разрешён"

Имя функции должно быть глаголом (сделать, получить) и на английском языке – getData, calculateSum. Логика не должна смешиваться – одна функция-одна задача. И чистые функции, конечно, предпочтительнее.


Каррирование

Например, есть такое понятие как каррирование — это процесс преобразования функции от нескольких аргументов в последовательность функций, каждая из которых принимает по одному аргументу.

Вместо того, чтобвы вызывать функцию так:

add(1, 2, 3)

Мы сможем вызывать так:

add(1)(2)(3)

И в дальнейшем это позволит нам «замораживать» аргументы, без необходимости передавать все три. Пример:

let addOne = add(1);
let addOneAndTwo = addOne(2);
let result = addOneAndTwo(3);

Шаблон таков - у нас есть функция:

function add(a, b, c) {
return a + b + c;
}

Мы создаём функцию curry, которая принимает исходную функцию и возвращает её каррированную версию:

function curry(fn) {
return function curried(...args) {
// Если передали достаточно аргументов — вызываем исходную функцию
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
// Иначе возвращаем функцию, которая ждёт остальные аргументы
return function (...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
}
}
};
}

Пояснение. fn.length это значение количества параметров у исходной функции (например, add(a,b,c) соответственно имеет fn.length === 3, т.е. три параметра). В …args мы собираем все переданные аргументы, и если их хватает - вызываем fn. Если не хватает, то возвращаем новую функцию, которая запомнит текущие фрагменты и дождётся остальных.

Пример, который мы привели выше (curry(fn)) это непростая функция, но более универсальная, способная работать с любым количеством аргументов. Можно сделать и более простой шаблон:

function curry3(f) {
return function(a) {
return function(b) {
return function(c) {
return f(a, b, c);
};
};
};
}

// Использование:
const curriedAdd = curry3(add);
curriedAdd(1)(2)(3); // → 6

Это не так универсально, но может быть нагляднее - здесь три аргумента. Есть и более современные способы, к примеру, при совмещении стрелочных функций и каррирования:

const curry = (fn) => (...args) =>
args.length >= fn.length
? fn(...args)
: (...nextArgs) => curry(fn)(...args, ...nextArgs);

Работает абсолютно так же, просто сделано в другом стиле.

Каррирование позволяет частично применять фунции, создавать специализированные функции без дублирования кода. Нужно взять функцию, обернуть её (как мы сделали в let addOne, к примеру), и затем вызывать.


Функции как объекты первого класса

В JavaScript функции являются полноценными объектами. Это означает, что функции можно присваивать переменным, передавать как аргументы в другие функции и возвращать из функций как результат.

Функции можно хранить в переменных:

// Обычная функция
function sayHello() {
console.log("Привет!");
}

// Присваиваем функцию переменной
const greet = sayHello;
greet(); // "Привет!"

Функции можно передавать как аргументы:

function execute(func) {
func();
}

function showMessage() {
console.log("Сообщение выполнено");
}

execute(showMessage); // "Сообщение выполнено"

Функции можно возвращать из других функций:

function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}

const double = createMultiplier(2);
console.log(double(5)); // 10

const triple = createMultiplier(3);
console.log(triple(5)); // 15

Функции можно хранить в массивах и объектах:

// Массив функций
const operations = [
function(a, b) { return a + b; },
function(a, b) { return a - b; },
function(a, b) { return a * b; }
];

console.log(operations[0](10, 5)); // 15
console.log(operations[1](10, 5)); // 5
console.log(operations[2](10, 5)); // 50

// Объект с функциями
const calculator = {
add: function(a, b) { return a + b; },
subtract: function(a, b) { return a - b; },
multiply: function(a, b) { return a * b; }
};

console.log(calculator.add(10, 5)); // 15
console.log(calculator.subtract(10, 5)); // 5
console.log(calculator.multiply(10, 5)); // 50

Функции могут иметь собственные свойства:

function counter() {
counter.count++;
return counter.count;
}

counter.count = 0;

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

Неявный return

Стрелочные функции в JavaScript поддерживают неявный возврат значения. Когда тело стрелочной функции состоит из одного выражения без фигурных скобок, результат этого выражения автоматически возвращается.

Простой пример неявного возврата:

// С фигурными скобками и явным return
const sum1 = (a, b) => {
return a + b;
};

// Без фигурных скобок - неявный return
const sum2 = (a, b) => a + b;

console.log(sum1(2, 3)); // 5
console.log(sum2(2, 3)); // 5

Неявный возврат особенно удобен для коротких функций:

// Умножение
const multiply = (a, b) => a * b;

// Возведение в квадрат
const square = x => x * x;

// Проверка чётности
const isEven = num => num % 2 === 0;

// Получение длины строки
const getLength = str => str.length;

console.log(multiply(4, 5)); // 20
console.log(square(5)); // 25
console.log(isEven(10)); // true
console.log(getLength("JavaScript")); // 10

При работе с объектами нужно использовать круглые скобки:

// Создание объекта с неявным возвратом
const createUser = (name, age) => ({ name, age });

const user = createUser("Анакин", 25);
console.log(user); // { name: "Анакин", age: 25 }

Неявный возврат упрощает работу с методами массивов:

const numbers = [1, 2, 3, 4, 5];

// map с неявным возвратом
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// filter с неявным возвратом
const evens = numbers.filter(num => num % 2 === 0);
console.log(evens); // [2, 4]

// find с неявным возвратом
const firstEven = numbers.find(num => num % 2 === 0);
console.log(firstEven); // 2

Контекст this в стрелочных функциях

Стрелочные функции ведут себя иначе, чем обычные функции, когда дело доходит до ключевого слова this. Стрелочные функции не создают собственный контекст выполнения и заимствуют this из внешней области видимости.

Обычные функции создают свой контекст this:

const person = {
name: "Люк Скайуокер",
regularMethod: function() {
console.log(this.name); // "this" указывает на объект person
},
arrowMethod: () => {
console.log(this.name); // "this" указывает на глобальный объект
}
};

person.regularMethod(); // "Люк Скайуокер"
person.arrowMethod(); // undefined (или ошибка в строгом режиме)

Стрелочные функции сохраняют контекст из внешней области:

const jedi = {
name: "Оби-Ван Кеноби",
sayName: function() {
// Обычная функция создает свой контекст
setTimeout(function() {
console.log(this.name); // undefined (this указывает на window)
}, 1000);

// Стрелочная функция сохраняет контекст
setTimeout(() => {
console.log(this.name); // "Оби-Ван Кеноби"
}, 1000);
}
};

jedi.sayName();

Пример с обработчиками событий:

const button = {
label: "Кнопка",
clickHandler: function() {
// Стрелочная функция сохраняет контекст button
document.getElementById("myButton").addEventListener("click", () => {
console.log(this.label); // "Кнопка"
});
}
};

button.clickHandler();

Стрелочные функции полезны в методах объектов, которые возвращают функции:

const spaceship = {
name: "Тысячелетний сокол",
getPilot: function() {
return () => {
return this.name; // Сохраняет контекст spaceship
};
}
};

const pilot = spaceship.getPilot();
console.log(pilot()); // "Тысячелетний сокол"

Пример с цепочкой вызовов:

const calculator = {
value: 0,
add: function(num) {
this.value += num;
return this;
},
multiply: function(num) {
this.value *= num;
return this;
},
getResult: () => {
return this.value; // this указывает не на calculator
}
};

// Работает с обычными функциями
const result = calculator.add(5).multiply(3);
console.log(result.value); // 15

// Не работает со стрелочной функцией
console.log(calculator.getResult()); // undefined

Стрелочные функции не подходят для конструкторов:

// Обычная функция как конструктор
function Jedi(name) {
this.name = name;
this.sayName = function() {
console.log(this.name);
};
}

const luke = new Jedi("Люк");
luke.sayName(); // "Люк"

// Стрелочная функция не может быть конструктором
const Sith = (name) => {
this.name = name; // this указывает не туда
};

// Это вызовет ошибку
// const vader = new Sith("Вейдер");

Ключевое различие:

  • Обычные функции создают свой контекст this при вызове
  • Стрелочные функции заимствуют this из места своего создания
  • Стрелочные функции не имеют arguments, super и new.target
  • Стрелочные функции всегда анонимны и не могут быть конструкторами

Выбор между обычными и стрелочными функциями зависит от задачи. Когда нужен динамический контекст this, используют обычные функции. Когда контекст должен сохраняться, выбирают стрелочные функции.